C++ 面向对象

最近更新于 2024-05-05 14:18

环境

Debian 11 (arm64)

编译器 g++ 10.2.1;编译标准 C++20;参数:-std=c++20 -no-pie -Wall -Werror=return-type -Werror=non-virtual-dtor -Werror=address -Werror=sequence-point -Werror=format-security -Wextra -pedantic -Wimplicit-fallthrough -Wsequence-point -Wswitch-unreachable -Wswitch-enum -Wstringop-truncation -Wbool-compare -Wtautological-compare -Wfloat-equal -Wshadow=global -Wpointer-arith -Wpointer-compare -Wcast-align -Wcast-qual -Wwrite-strings -Wdangling-else -Wlogical-op -Wconversion -g -O0

class 和 struct

在 C 语言中并没有 class 这个关键词,但是有 struct 结构体。C 中也是存在面向对象设计思想的,而实现载体就是 struct。
比如,C 语言中的 FILE 文件指针,通过 struct 封装了文件都具有的一些属性,操作文件的打开、读、写、关闭方法都封装了对应的函数。不是说一定要显式的搞个什么类才叫面向对象,更重要的是这种思想。

C++ 中有了 class,而 struct 也是 C++ 中的类,只是它们的默认属性不同,class 默认私有,struct 默认公有。
file
file

构造函数和析构函数

#include <iostream>

class Person
{
    private: // 私有,仅内部成员可以访问
        std::string m_name;

    public:
        /**
         * @brief 默认构造函数,
         *      在实例化对象时自动执行,
         *      一般用于默认初始化,
         *      如果没有定义,编译器也会自动添加一个
        */
        Person();

        /**
         * @brief 拷贝构造函数,
         *      该函数实现将一个对象的属性值复制给另外一个同类对象,
         *      如果没有定义,编译器会自动添加一个,默认一一对应拷贝变量值
         * @param person 拷贝对象
        */
        Person(const Person &person);

        /**
         * @brief 设置名字
         * @param name 名字
        */
        void setName(std::string name);

        /**
         * @brief 获取名字
         * @return 名字
        */
        std::string getName();

        /**
         * @brief 析构函数,
         *      在实例对象被销毁时自动执行,
         *      一般用于清理善后,
         *      如果没有定义,编译器也会自动添加一个
        */
        ~Person();
};

Person::Person()
{
    m_name = "空";
    std::cout << "默认构造函数" << std::endl;
}

Person::Person(const Person &person)
{
    m_name = person.m_name;
    std::cout << "拷贝构造函数" << std::endl;
}

void Person::setName(std::string name)
{
    m_name = name;
}

std::string Person::getName()
{
    return m_name;
}

Person::~Person()
{
    std::cout << "析构函数" << std::endl;
}

int main()
{
    Person p1; // 默认构造
    p1.setName("小明");

    Person p2(p1); // 拷贝构造
    std::cout << p2.getName() << std::endl;
}

file

拷贝构造函数

上一个例子也定义了拷贝构造函数,即使不定义,编译器也会自动添加。(编译器自动添加:构造函数、拷贝构造函数、析构函数和赋值重载)

编译器添加的拷贝构造函数是浅拷贝,如果类成员变量不是存储在堆区还好,如果是的话,使用浅拷贝时就相当于复制地址,等同于另起了一个别名指向同一块内存,在析构释放内存的时候就有可能出现重复 delete 的问题。所以需要自己写深拷贝构造函数实现,拷贝时新申请一块内存地址,再赋值进去。

#include <iostream>

class MyClass
{
    private:
        int *m_num = NULL;

    public:
        MyClass()
        {
            m_num = new int(0);
        }

        MyClass(int num)
        {
            if (m_num != NULL)
            {
                delete m_num;
                m_num = NULL;
            }
            m_num = new int(num);
        }

        int get()
        {
            return *m_num;
        }

        MyClass(MyClass &mc)
        {
            m_num = new int(mc.get());
        }

        ~MyClass()
        {
            if (m_num != NULL)
            {
                delete m_num;
                m_num = NULL;
            }
        }
};

int main()
{
    MyClass mc1(10);
    MyClass mc2(mc1);
    MyClass mc3 = mc1;

    std::cout << mc2.get() << " " << mc3.get() << std::endl;
}

file

当我注释掉自定义的拷贝构造函数以后,编译运行时就会出现错误,错误的位置就在 delete
file

匿名对象

通过 “类名()” 方式使用,而不实例化对象名,这种方式使用调用完当前语句空间就会被释放,即为匿名对象。

#include <iostream>

class MyClass
{
    public:
        void print(std::string s)
        {
            std::cout << s << std::endl;
        }
};

int main()
{
    MyClass().print("Hello world!"); // 匿名对象

    MyClass mc;
    mc.print("你好,世界!");
}

file

static 成员

静态成员变量

静态成员变量不管有多少个对象,实际在内存中只有一份,在编译的时候就给分配好了内存空间。即使没有实例化的对象(如果是公开的成员的话),可以直接通过 类型::变量名 进行使用,如果有实例的对象的话,也可以通过 对象名::变量名 使用,它们实际都是对同一个变量进行操作,即共用一个变量。

#include <iostream>

class ClassName
{
    private:
        static int m_num1;

    public:
        static int m_num2;

        void setNum(int num);
        int getNum();
};

// 静态变量在类外初始化
int ClassName::m_num1 = 0;
int ClassName::m_num2 = 0;

void ClassName::setNum(int num) // 写私有成员
{
    m_num1 = num;
}

int ClassName::getNum() // 读私有成员
{
    return m_num1;
}

int main()
{
    ClassName c1;
    c1.setNum(77);
    c1.m_num2 = 99;

    ClassName c2;
    std::cout << "c2.m_num1:" << c2.getNum() << "\nc2.m_num2:" << c2.m_num2 << "\nClassName::m_num2:" << ClassName::m_num2 << std::endl;
}

file

上面的例子,我实例化了一个 c1,并通过 c1 为静态成员变量设置了值。然后又实例化了一个对象 c2,通过 c2 获取静态成员变量的值就是前面通过 c1 设置的。使用类名也可以直接读取前面设置的公开静态成员变量。

下面又一个例子,深入分析静态成员变量

#include <iostream>

class C1
{
    static int num1;
    int num2;
};

class C2
{
    int num1;
    int num2;
};

int main()
{
    C1 c1;
    C2 c2;

    std::cout << "int " << sizeof(int) << std::endl;
    std::cout << "C1 " << sizeof(C1) << " c1 " << sizeof(c1) << std::endl;
    std::cout << "C2 " << sizeof(C2) << " c2 " << sizeof(c2) << std::endl;
}

file

int 4 字节,C1 类中一个静态变量,一个普通变量,总大小为 4 字节 ,与之对比的 C2 类中是两个普通变量,总大小为 8 字节。
则说明静态变量实际并不在类中,是独立于类开辟的储存空间。应该是和普通静态变量一样,都是存放在全局数据区,和全局变量一样,编译的时候就已经分配好了内存空间,而不是运行到定义的地方才分配。所以即使没有实例化对象也能对它进行操作,只是被挂到类上进行使用,对它的操作很多时候就和非成员变量差不多。

静态成员函数

静态成员函数不能访问类中的变量,可以通过类名调用,也可以通过实例对象调用。

#include <iostream>

class ClassName
{
    public:
        static void fun1();
};

void ClassName::fun1()
{
    std::cout << "静态成员函数" << std::endl;
}

int main()
{
    ClassName c;
    c.fun1();

    ClassName::fun1();
}

file

const 成员

常成员变量

使用 const 修饰的变量只能初始化一次,不能修改值。若要对常成员变量初始化为指定值,就只能通过构造函数参数初始化列表进行

#include <iostream>

class ClassName
{
    private:
        const int m_num1;

    public:
        const int  m_num2;

        ClassName(int num1, int num2);
        int getNum1();
};

ClassName::ClassName(int num1, int num2) : m_num1(num1), m_num2(num2)
{
    ;
}

int ClassName::getNum1()
{
    return m_num1;
}

int main()
{
    ClassName c(1, 2);
    std::cout << "m_num1=" << c.getNum1() << "\n" << "m_num2=" << c.m_num2 << std::endl;
}

file

常成员函数

常成员函数内部可以读成员变量值,但是无法向成员变量写入,即成员变量在常成员函数内部具有只读属性。

#include <iostream>

class ClassName
{
    private:
        int m_num;

    public:
        void fun() const;
};

void ClassName::fun() const
{
    m_num = 1;
}

int main()
{

}

file

友元

友元函数

在类中使用 friend 声明一个非成员函数后,该函数就可以访问该类的私有成员了

#include <iostream>

class ClassName
{
    private:
        int m_num;
        friend int getNum(ClassName c);

    public:
        ClassName();
};

ClassName::ClassName()
{
    m_num = 6;
}

int getNum(ClassName c)
{
    return c.m_num;
}

int main()
{
    ClassName c;
    std::cout << getNum(c) << std::endl;
}

file

友元类

在一个类中声明另外一个类为友元,则被声明的类中可以使用声明者的私有成员

#include <iostream>

class A
{
    private:
        int numA;

    public:
        friend class B;
        A();
};

A::A()
{
    numA = 1;
}

class B
{
    private:
        int numB;

    public:
        friend class C;
        B();
        int get(A a);
};

B::B()
{
    numB = 2;
}

int B::get(A a)
{
    return a.numA;
}

class C
{
    public:
        int get(B b);
};

int C::get(B b)
{
    return b.numB;
}

int main()
{
    A a;
    B b;
    C c;

    std::cout << b.get(a) << std::endl;
    std::cout << c.get(b) << std::endl;
}

这里有三个类,分别为 A、B 和 C。A 中有私有变量 numA 初始化为 1,B 中有私有变量 numB 初始化为 2。A 类中声明 B 为友元类,B 类中声明 C 为友元类。则 C 类可以使用 B 类的私有成员,B 类可以使用 A 类的私有成员。

file

继承与派生

一个类继承另外一个类后,就可以一定程度上使用它的成员。一般用于扩充功能,父类实现了一些功能,子类继承父类继续添加新功能。
两者的称呼一般有两套
若 B 从 A 继承

A B
父类 子类
基类 派生类

权限

类的权限有三种:

  • public 公有:类内、类外和子类都可访问
  • private 私有:仅类内可以访问
  • protected 保护:类内和子类可以访问

继承后的权限

父类权限 \ 继承权限 public private protected
public public private protected
private 不可访问 不可访问 不可访问
protected protected private protected

权限严格程度:private > protected > public,哪个严格,继承后就是哪种权限,父类为私有的就完全不能继承给子类。

单继承

一个子类只继承一个父类。这里写一个多级继承的例子,B 从 A 继承,C 从 B 继承,即 A->B->C

#include <iostream>

class A
{
    public:
        A()
        {
            std::cout << "A 构造函数" << std::endl;
        }

        ~A()
        {
            std::cout << "A 析构函数" << std::endl;
        }
};

class B : public A // 从 A 继承
{
    public:
        B()
        {
            std::cout << "B 构造函数" << std::endl;
        }

        ~B()
        {
            std::cout << "B 析构函数" << std::endl;
        }
};

class C : public B // 从 B 继承
{
    public:
        C()
        {
            std::cout << "C 构造函数" << std::endl;
        }

        ~C()
        {
            std::cout << "C 析构函数" << std::endl;
        }
};

int main()
{
    C c;
}

file

多继承

一个子类继承多个父类。这里写一个例子 C 从 A 和 B 继承

#include <iostream>

class A
{
    public:
        A()
        {
            std::cout << "A 构造函数" << std::endl;
        }

        ~A()
        {
            std::cout << "A 析构函数" << std::endl;
        }
};

class B
{
    public:
        B()
        {
            std::cout << "B 构造函数" << std::endl;
        }

        ~B()
        {
            std::cout << "B 析构函数" << std::endl;
        }
};

class C : public A, public B // 从 A 和 B 继承
{
    public:
        C()
        {
            std::cout << "C 构造函数" << std::endl;
        }

        ~C()
        {
            std::cout << "C 析构函数" << std::endl;
        }
};

int main()
{
    C c;
}

file

子类父类同名变量

父类变量和子类同名时,默认使用子类的,访问父类的需要指定作用域

#include <iostream>

class A
{
    public:
        int num;

        int getA()
        {
            return num;
        }
};

class B
{
    public:
        int num;

        int getB()
        {
            return num;
        }
};

class C : public A, public B
{
    public:
        int num;

        int getC()
        {
            return num;
        }
};

int main()
{
    C c;
    c.num = 10;
    c.A::num = 9;
    c.B::num = 8;

    std::cout << c.getA() << " " << c.getB() << " " << c.getC() << std::endl;
}

file

菱形继承

file

如图中,A 有一个变量 n,分别继承给 A1 和 A2,AA 又从 A1 和 A2 继承,则 AA 中会分别有来自 A1 和 A2 的两个 n。但是实际只需要使用一个 n,这样重复就会浪费内存资源。
这里用代码复现一下上图的菱形继承

#include <iostream>

class A
{
    public:
        int n;
};

class A1 : public A
{
};

class A2 : public A
{
};

class AA : public A1, public A2
{
};

int main()
{
    AA aa;
    aa.A1::n = 1;
    aa.A2::n = 2;
    std::cout << aa.A1::n << " " << aa.A2::n << std::endl;
}

file

虚继承

而通过虚继承就可以解决这个问题,在 A1 从 A 继承和 A2 从 A 继承时都使用 virtual 修饰,那么最终继承到 AA 时,最终只会分配一个 n。

#include <iostream>

class A
{
    public:
        int n;
};

class A1 : virtual public A
{
};

class A2 : virtual public A
{
};

class AA : public A1, public A2
{
};

int main()
{
    AA aa;
    aa.A1::n = 1;
    std::cout << aa.n << std::endl;
    aa.A2::n = 2;
    std::cout << aa.n << std::endl;
}

file

多态

多态分为静态多态和动态多态。静态多态在编译时就确定了函数地址,比如函数重载和运算符重载,复用名字,但是编译时就确定了要调用的是谁;而动态多态,则是在运行时才确定函数地址,比如派生类和虚函数实现的运行时多态。

#include <iostream>

class Person
{
    public:
        void speak()
        {
            std::cout << "说话" << std::endl;
        }
};

class Xiaoming : public Person
{
    public:
        void speak()
        {
            std::cout << "小明说话" << std::endl;
        }
};

class Xiaohong : public Person
{
    public:
        void speak()
        {
            std::cout << "小红说话" << std::endl;
        }
};

int main()
{
    Xiaoming xm;
    Xiaohong xh;
    Person &p1 = xm;
    Person &p2 = xh;

    p1.speak();
    p2.speak();

}

file

尽管指向的是两个子类,但是实际执行的还是父类的函数,这是因为编译的时候确定了调用函数的地址。

#include <iostream>

class Person
{
    public:
        virtual void speak()
        {
            std::cout << "说话" << std::endl;
        }

        virtual ~Person()
        {
        }
};

class Xiaoming : public Person
{
    public:
        void speak()
        {
            std::cout << "小明说话" << std::endl;
        }
};

class Xiaohong : public Person
{
    public:
        void speak()
        {
            std::cout << "小红说话" << std::endl;
        }
};

int main()
{
    Xiaoming xm;
    Xiaohong xh;
    Person &p1 = xm;
    Person &p2 = xh;

    p1.speak();
    p2.speak();
}

file

通过 virtual 修饰函数后(析构函数也要),则做到动态多态,运行时确定指向的函数地址。

纯虚函数(及纯虚析构)

前面写的例子的时候,父类的函数也给了定义,但是实际我们通常希望的是通过子类来实现具体的功能,父类不需要实现,此时就可以用到纯虚函数,直接在后面写上 “= 0” 就行。
另外存在纯虚函数的类为抽象类,无法实例化对象。

#include <iostream>

class Person
{
    public:
        virtual void speak() = 0; // 纯虚函数
        virtual ~Person() = 0; // 纯虚析构
};

Person::~Person() {} // 纯虚析构也必须要定义函数实体

class Xiaoming : public Person
{
    private:
        std::string *s;

    public:
        Xiaoming()
        {
            s = new std::string("小明");
            std::cout << "小明构造" << std::endl;
        }

        void speak()
        {
            std::cout << *s << std::endl;
        }

        ~Xiaoming()
        {
            delete s;
            std::cout << "小明析构" << std::endl;
        }
};

class Xiaohong : public Person
{
    private:
        std::string *s;

    public:
        Xiaohong()
        {
            s = new std::string("小红");
            std::cout << "小红构造" << std::endl;
        }

        void speak()
        {
            std::cout << *s << std::endl;
        }

        ~Xiaohong()
        {
            delete s;
            std::cout << "小红析构" << std::endl;
        }
};

int main()
{
    Person *p = new Xiaoming;
    p->speak();
    delete p;

    p = new Xiaohong;
    p->speak();
    delete p;
}

file

运算符重载

下面只举部分例子用作参考

+ 号重载

这里写了一个例子,类中有两个数,通过对两个实例对象相加,实现两个对象的两个对应数字相加。C++ 中原生的 + 号并不能直接满足这里的要求,就需要自行重写一个 + 号的功能满足这里的条件。

成员函数实现

#include <iostream>

class MyClass
{
    private:
        int m_num1;
        int m_num2;

    public:
        MyClass()
        {
            m_num1 = 0;
            m_num2 = 0;
        }

        void set1(int num)
        {
            m_num1 = num;
        }

        void set2(int num)
        {
            m_num2 = num;
        }

        int get1()
        {
            return m_num1;
        }

        int get2()
        {
            return m_num2;
        }

        MyClass operator+(MyClass &mc)
        {
            MyClass tmp;
            tmp.set1(m_num1 + mc.get1());
            tmp.set2(m_num2 + mc.get2());
            return tmp;
        }
};

int main()
{
    MyClass a, b, c;
    a.set1(1);
    a.set2(2);
    b.set1(3);
    b.set2(5);

    // c = a.operator+(b); // 本质上的形式,下面是一般使用形式
    c = a + b;
    std::cout << c.get1() << " " << c.get2() << std::endl;
}

file

全局函数实现

#include <iostream>

class MyClass
{
    private:
        int m_num1;
        int m_num2;

    public:
        MyClass()
        {
            m_num1 = 0;
            m_num2 = 0;
        }

        void set1(int num)
        {
            m_num1 = num;
        }

        void set2(int num)
        {
            m_num2 = num;
        }

        int get1()
        {
            return m_num1;
        }

        int get2()
        {
            return m_num2;
        }
};

MyClass operator+(MyClass &mc1, MyClass &mc2)
{
    MyClass tmp;
    tmp.set1(mc1.get1() + mc2.get1());
    tmp.set2(mc1.get2() + mc2.get2());
    return tmp;
}

int main()
{
    MyClass a, b, c;
    a.set1(1);
    a.set2(2);
    b.set1(3);
    b.set2(5);

    // c = operator+(a, b); // 本质上的形式,下面是一般使用形式
    c = a + b;
    std::cout << c.get1() << " " << c.get2() << std::endl;
}

<< 重载

前面的例子实现了两个对象相加,实际内部两个数对应相加,但是要输出显示时要手动取获取两个值比较麻烦,所以这里设法重载 <<,实现直接输出显示对象内容。
这里重载不同于前面,如果使用成员函数重载的形式,必须有一个参数,那么使用时就是 “对象1.operator<<(对象2)” 的实质形式了。这里使用需要的形式是 “std::cout << 对象”,所以得使用全局函数来重载,此时必须要有两个参数。第一个参数其实就是要引用 std::cout(它其实是 std::ostream 类型的输出流),第二参数接要显示内容的对象,要显示的内容流入到引用的 std::cout 中,再将它的引用返回,则外部调用时可以继续在它后面 << 添加要显示的内容。

#include <iostream>

class MyClass
{
    private:
        int m_num1;
        int m_num2;

    public:
        MyClass()
        {
            m_num1 = 0;
            m_num2 = 0;
        }

        void set1(int num)
        {
            m_num1 = num;
        }

        void set2(int num)
        {
            m_num2 = num;
        }

        int get1()
        {
            return m_num1;
        }

        int get2()
        {
            return m_num2;
        }

        MyClass operator+(MyClass &mc)
        {
            MyClass tmp;
            tmp.set1(m_num1 + mc.get1());
            tmp.set2(m_num2 + mc.get2());
            return tmp;
        }
};

std::ostream &operator<<(std::ostream &cout, MyClass &mc)
{
    cout << "num1 " << mc.get1() << ",num2 " << mc.get2();
    return cout;
}

int main()
{
    MyClass a, b, c;
    a.set1(1);
    a.set2(2);
    b.set1(3);
    b.set2(5);

    // c = a.operator+(b); // 本质上的形式,下面是一般使用形式
    c = a + b;
    std::cout << c << std::endl;
}

file

++ 重载

#include <iostream>

class MyInt
{
    private:
        int m_num;

    public:
        MyInt(int num)
        {
            m_num = num;
        }

        int get()
        {
            return m_num;
        }

        MyInt &operator++() // 前置
        {
            ++m_num;
            return *this; // 返回对象自己
        }

        MyInt operator++(int) // 后置
        {
            MyInt tmp = *this; // 先保存旧对象中的值
            ++m_num;
            return tmp;
        }

};

std::ostream &operator<<(std::ostream &cout, MyInt mi)
{
    cout << mi.get();
    return cout;
}

int main()
{
    MyInt mi(8);
    std::cout << ++mi << std::endl;
    std::cout << mi++ << std::endl;
    std::cout << mi << std::endl;
}

file

= 重载

赋值重载这里考虑的情况和拷贝构造类似,如果不涉及使用堆区,默认的浅拷贝赋值操作也能满足要求。如果有在堆区开辟内存,那么就得实现深拷贝了。

= 重载只能通过成员函数实现

#include <iostream>

class MyClass
{
    private:
        int *m_num = NULL;

    public:
        MyClass()
        {
            m_num = new int(0);
        }

        MyClass(int num)
        {
            if (m_num != NULL)
            {
                delete m_num;
                m_num = NULL;
            }
            m_num = new int(num);
        }

        int get()
        {
            return *m_num;
        }

        ~MyClass()
        {
            if (m_num != NULL)
            {
                delete m_num;
                m_num = NULL;
            }
        }

        MyClass &operator=(MyClass &mc)
        {
            if (m_num != NULL)
            {
                delete m_num;
                m_num = NULL;
            }
            m_num = new int(mc.get());

            return *this;
        }
};

std::ostream &operator<<(std::ostream &cout, MyClass &mc)
{
    cout << mc.get();
    return cout;
}

int main()
{
    MyClass mc1(9);
    MyClass mc2, mc3;

    mc3 = mc2 = mc1;
    std::cout << mc3 << " " << mc2 << std::endl;
}

file

> 重载

这里举一个例子,定义一个类,有名字和年龄两个属性,重载大于符号实现两个对象的年龄比较。

#include <iostream>

class Person
{
    private:
        std::string m_name;
        int m_age;

    public:
        Person(std::string name, int age)
        {
            m_name = name;
            m_age = age;
        }

        int getAge()
        {
            return m_age;
        }

        bool operator>(Person &p)
        {
            if (m_age > p.getAge())
            {
                return true;
            }
            return false;
        }
};

int main()
{
    Person p1("小明", 20);
    Person p2("小红", 19);
    Person p3("小强", 21);

    std::cout << (p1 > p2) << std::endl;
    std::cout << (p1 > p3) << std::endl;
}

file

() 重载

函数调用运算符 () 重载,也叫做仿函数

#include <iostream>

class Add
{
    public:
        int operator()(int num1, int num2)
        {
            return num1 + num2;
        }
};

class Print
{
    public:
        void operator()(std::string s)
        {
            std::cout << s << std::endl;
        }
};

int main()
{
    Add add;
    std::cout << add(9, 1) << std::endl; // 调用仿函数
    std::cout << Add()(1, 99) << std::endl; // 匿名对象-仿函数

    Print print;
    print("Hello world!"); // 调用仿函数

}

file

C++ 面向对象
Scroll to top